Micron Document
๐ŸŽ–๏ธGitะฏั€ะฐ๐ŸŽ–๏ธ

Node / meshtastic / Meshtastic-Android / files / core / ui / src / commonTest / kotlin / org / meshtastic / core / ui / util / NetworkTransportTest.kt

Displaying Raw โ€ข Download

core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/util/NetworkTransportTest.kt 9613ba91a7e169afe8ce2e98c6d384ef37f03421 (9613ba91) Text, 13.27 KB

T8b949e/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Tff7b72package T7ee787org.meshtastic.core.ui.util

Tff7b72import T7ee787kotlin.test.Test
Tff7b72import T7ee787kotlin.test.assertFalse
Tff7b72import T7ee787kotlin.test.assertTrue

T8b949e/**
* Unit coverage for the platform-agnostic reduction that drives `isWifiUnavailable()` on Android. `ConnectivityManager`
* itself is not unit-testable here (it needs an Android runtime), so these cases exercise the pure helper that the
* Android actual delegates to. The predicate: network-scan transport is available when any current network has Wi-Fi,
* Ethernet, or VPN; cellular-only is insufficient. Each case mirrors a real connectivity snapshot the
* `ConnectionsScreen` recovery banner must react to correctly.
*/
Tff7b72class T56d364NetworkTransportTest Tb4b4b4{
Tf0883e@Test
Tff7b72fun Td2a8ffwifi_network_present_then_local_availableTb4b4b4(Tb4b4b4) Tb4b4b4{
T8b949e// Single Wi-Fi network: banner should clear.
Te6edf3assertTrueTb4b4b4(
Te6edf3anyNetworkScanTransportAvailableTb4b4b4(
Te6edf3listOfTb4b4b4(Te6edf3NetworkTransportInfoTb4b4b4(Te6edf3hasWifi Tff7b72= Tff7b72trueTb4b4b4, Te6edf3hasEthernet Tff7b72= Tff7b72falseTb4b4b4, Te6edf3hasVpn Tff7b72= Tff7b72falseTb4b4b4)Tb4b4b4)Tb4b4b4,
Tb4b4b4)Tb4b4b4,
Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ffethernet_network_present_then_local_availableTb4b4b4(Tb4b4b4) Tb4b4b4{
T8b949e// Ethernet (e.g. desktop dock / Android tablet on wired LAN) also carries NSD/mDNS traffic.
Te6edf3assertTrueTb4b4b4(
Te6edf3anyNetworkScanTransportAvailableTb4b4b4(
Te6edf3listOfTb4b4b4(Te6edf3NetworkTransportInfoTb4b4b4(Te6edf3hasWifi Tff7b72= Tff7b72falseTb4b4b4, Te6edf3hasEthernet Tff7b72= Tff7b72trueTb4b4b4, Te6edf3hasVpn Tff7b72= Tff7b72falseTb4b4b4)Tb4b4b4)Tb4b4b4,
Tb4b4b4)Tb4b4b4,
Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ffonly_cellular_network_then_local_unavailableTb4b4b4(Tb4b4b4) Tb4b4b4{
T8b949e// Cellular-only: no LAN, banner should show.
Te6edf3assertFalseTb4b4b4(
Te6edf3anyNetworkScanTransportAvailableTb4b4b4(
Te6edf3listOfTb4b4b4(Te6edf3NetworkTransportInfoTb4b4b4(Te6edf3hasWifi Tff7b72= Tff7b72falseTb4b4b4, Te6edf3hasEthernet Tff7b72= Tff7b72falseTb4b4b4, Te6edf3hasVpn Tff7b72= Tff7b72falseTb4b4b4)Tb4b4b4)Tb4b4b4,
Tb4b4b4)Tb4b4b4,
Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ffwifi_present_as_non_default_alongside_cellular_then_local_availableTb4b4b4(Tb4b4b4) Tb4b4b4{
T8b949e// The regression case: cellular is the system default (or Wi-Fi is unvalidated), so the
T8b949e// previous `activeNetwork` check missed Wi-Fi. With allNetworks scanning, the banner clears.
Tff7b72val Te6edf3cellular Tff7b72= Te6edf3NetworkTransportInfoTb4b4b4(Te6edf3hasWifi Tff7b72= Tff7b72falseTb4b4b4, Te6edf3hasEthernet Tff7b72= Tff7b72falseTb4b4b4, Te6edf3hasVpn Tff7b72= Tff7b72falseTb4b4b4)
Tff7b72val Te6edf3wifi Tff7b72= Te6edf3NetworkTransportInfoTb4b4b4(Te6edf3hasWifi Tff7b72= Tff7b72trueTb4b4b4, Te6edf3hasEthernet Tff7b72= Tff7b72falseTb4b4b4, Te6edf3hasVpn Tff7b72= Tff7b72falseTb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3anyNetworkScanTransportAvailableTb4b4b4(Te6edf3listOfTb4b4b4(Te6edf3cellularTb4b4b4, Te6edf3wifiTb4b4b4)Tb4b4b4)Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ffno_networks_then_local_unavailableTb4b4b4(Tb4b4b4) Tb4b4b4{
T8b949e// Airplane mode / no connectivity at all.
Te6edf3assertFalseTb4b4b4(Te6edf3anyNetworkScanTransportAvailableTb4b4b4(Te6edf3emptyListTb4b4b4(Tb4b4b4)Tb4b4b4)Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ffwifi_lost_across_all_networks_then_local_unavailableTb4b4b4(Tb4b4b4) Tb4b4b4{
T8b949e// Previously had Wi-Fi, now every tracked network lacks every scan transport: banner returns.
Tff7b72val Te6edf3allDropped Tff7b72=
Te6edf3listOfTb4b4b4(
Te6edf3NetworkTransportInfoTb4b4b4(Te6edf3hasWifi Tff7b72= Tff7b72falseTb4b4b4, Te6edf3hasEthernet Tff7b72= Tff7b72falseTb4b4b4, Te6edf3hasVpn Tff7b72= Tff7b72falseTb4b4b4)Tb4b4b4,
Te6edf3NetworkTransportInfoTb4b4b4(Te6edf3hasWifi Tff7b72= Tff7b72falseTb4b4b4, Te6edf3hasEthernet Tff7b72= Tff7b72falseTb4b4b4, Te6edf3hasVpn Tff7b72= Tff7b72falseTb4b4b4)Tb4b4b4,
Tb4b4b4)
Te6edf3assertFalseTb4b4b4(Te6edf3anyNetworkScanTransportAvailableTb4b4b4(Te6edf3allDroppedTb4b4b4)Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ffwifi_restored_as_non_default_after_loss_then_local_availableTb4b4b4(Tb4b4b4) Tb4b4b4{
T8b949e// Symmetric to `wifi_lost_...`: after Wi-Fi returns (even as a non-default network), banner
T8b949e// clears again. Encoded as a state transition through the pure function.
Tff7b72val Te6edf3duringOutage Tff7b72= Te6edf3listOfTb4b4b4(Te6edf3NetworkTransportInfoTb4b4b4(Te6edf3hasWifi Tff7b72= Tff7b72falseTb4b4b4, Te6edf3hasEthernet Tff7b72= Tff7b72falseTb4b4b4, Te6edf3hasVpn Tff7b72= Tff7b72falseTb4b4b4)Tb4b4b4)
Te6edf3assertFalseTb4b4b4(Te6edf3anyNetworkScanTransportAvailableTb4b4b4(Te6edf3duringOutageTb4b4b4)Tb4b4b4)
Tff7b72val Te6edf3afterRecovery Tff7b72=
Te6edf3listOfTb4b4b4(
Te6edf3NetworkTransportInfoTb4b4b4(Te6edf3hasWifi Tff7b72= Tff7b72falseTb4b4b4, Te6edf3hasEthernet Tff7b72= Tff7b72falseTb4b4b4, Te6edf3hasVpn Tff7b72= Tff7b72falseTb4b4b4)Tb4b4b4,
Te6edf3NetworkTransportInfoTb4b4b4(Te6edf3hasWifi Tff7b72= Tff7b72trueTb4b4b4, Te6edf3hasEthernet Tff7b72= Tff7b72falseTb4b4b4, Te6edf3hasVpn Tff7b72= Tff7b72falseTb4b4b4)Tb4b4b4,
Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3anyNetworkScanTransportAvailableTb4b4b4(Te6edf3afterRecoveryTb4b4b4)Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ffvpn_only_then_local_availableTb4b4b4(Tb4b4b4) Tb4b4b4{
T8b949e// ZeroTier/Tailscale carry TCP traffic to a node over the routed overlay; banner should clear
T8b949e// even when no physical LAN transport is present.
Te6edf3assertTrueTb4b4b4(
Te6edf3anyNetworkScanTransportAvailableTb4b4b4(
Te6edf3listOfTb4b4b4(Te6edf3NetworkTransportInfoTb4b4b4(Te6edf3hasWifi Tff7b72= Tff7b72falseTb4b4b4, Te6edf3hasEthernet Tff7b72= Tff7b72falseTb4b4b4, Te6edf3hasVpn Tff7b72= Tff7b72trueTb4b4b4)Tb4b4b4)Tb4b4b4,
Tb4b4b4)Tb4b4b4,
Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ffwifi_and_vpn_then_local_availableTb4b4b4(Tb4b4b4) Tb4b4b4{
T8b949e// Wi-Fi plus an active VPN overlay: both transports independently clear the banner.
Tff7b72val Te6edf3wifi Tff7b72= Te6edf3NetworkTransportInfoTb4b4b4(Te6edf3hasWifi Tff7b72= Tff7b72trueTb4b4b4, Te6edf3hasEthernet Tff7b72= Tff7b72falseTb4b4b4, Te6edf3hasVpn Tff7b72= Tff7b72falseTb4b4b4)
Tff7b72val Te6edf3vpn Tff7b72= Te6edf3NetworkTransportInfoTb4b4b4(Te6edf3hasWifi Tff7b72= Tff7b72falseTb4b4b4, Te6edf3hasEthernet Tff7b72= Tff7b72falseTb4b4b4, Te6edf3hasVpn Tff7b72= Tff7b72trueTb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3anyNetworkScanTransportAvailableTb4b4b4(Te6edf3listOfTb4b4b4(Te6edf3wifiTb4b4b4, Te6edf3vpnTb4b4b4)Tb4b4b4)Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ffethernet_and_vpn_then_local_availableTb4b4b4(Tb4b4b4) Tb4b4b4{
T8b949e// Wired plus VPN: same as the Wi-Fi+VPN case for a docked desktop/tablet.
Tff7b72val Te6edf3ethernet Tff7b72= Te6edf3NetworkTransportInfoTb4b4b4(Te6edf3hasWifi Tff7b72= Tff7b72falseTb4b4b4, Te6edf3hasEthernet Tff7b72= Tff7b72trueTb4b4b4, Te6edf3hasVpn Tff7b72= Tff7b72falseTb4b4b4)
Tff7b72val Te6edf3vpn Tff7b72= Te6edf3NetworkTransportInfoTb4b4b4(Te6edf3hasWifi Tff7b72= Tff7b72falseTb4b4b4, Te6edf3hasEthernet Tff7b72= Tff7b72falseTb4b4b4, Te6edf3hasVpn Tff7b72= Tff7b72trueTb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3anyNetworkScanTransportAvailableTb4b4b4(Te6edf3listOfTb4b4b4(Te6edf3ethernetTb4b4b4, Te6edf3vpnTb4b4b4)Tb4b4b4)Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ffcellular_and_vpn_then_local_availableTb4b4b4(Tb4b4b4) Tb4b4b4{
T8b949e// The original bug report: cellular is the only physical uplink, but a VPN rides on top of it
T8b949e// and that VPN is the reachability path to the node. VPN clears the banner despite cellular.
Tff7b72val Te6edf3cellular Tff7b72= Te6edf3NetworkTransportInfoTb4b4b4(Te6edf3hasWifi Tff7b72= Tff7b72falseTb4b4b4, Te6edf3hasEthernet Tff7b72= Tff7b72falseTb4b4b4, Te6edf3hasVpn Tff7b72= Tff7b72falseTb4b4b4)
Tff7b72val Te6edf3vpn Tff7b72= Te6edf3NetworkTransportInfoTb4b4b4(Te6edf3hasWifi Tff7b72= Tff7b72falseTb4b4b4, Te6edf3hasEthernet Tff7b72= Tff7b72falseTb4b4b4, Te6edf3hasVpn Tff7b72= Tff7b72trueTb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3anyNetworkScanTransportAvailableTb4b4b4(Te6edf3listOfTb4b4b4(Te6edf3cellularTb4b4b4, Te6edf3vpnTb4b4b4)Tb4b4b4)Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ffvpn_lost_leaving_cellular_only_then_local_unavailableTb4b4b4(Tb4b4b4) Tb4b4b4{
T8b949e// Symmetric to `cellular_and_vpn_...`: when the VPN drops, only cellular remains, and the
T8b949e// banner must return. Encoded as a state transition through the pure function.
Tff7b72val Te6edf3withVpn Tff7b72=
Te6edf3listOfTb4b4b4(
Te6edf3NetworkTransportInfoTb4b4b4(Te6edf3hasWifi Tff7b72= Tff7b72falseTb4b4b4, Te6edf3hasEthernet Tff7b72= Tff7b72falseTb4b4b4, Te6edf3hasVpn Tff7b72= Tff7b72falseTb4b4b4)Tb4b4b4,
Te6edf3NetworkTransportInfoTb4b4b4(Te6edf3hasWifi Tff7b72= Tff7b72falseTb4b4b4, Te6edf3hasEthernet Tff7b72= Tff7b72falseTb4b4b4, Te6edf3hasVpn Tff7b72= Tff7b72trueTb4b4b4)Tb4b4b4,
Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3anyNetworkScanTransportAvailableTb4b4b4(Te6edf3withVpnTb4b4b4)Tb4b4b4)
Tff7b72val Te6edf3afterVpnLost Tff7b72= Te6edf3listOfTb4b4b4(Te6edf3NetworkTransportInfoTb4b4b4(Te6edf3hasWifi Tff7b72= Tff7b72falseTb4b4b4, Te6edf3hasEthernet Tff7b72= Tff7b72falseTb4b4b4, Te6edf3hasVpn Tff7b72= Tff7b72falseTb4b4b4)Tb4b4b4)
Te6edf3assertFalseTb4b4b4(Te6edf3anyNetworkScanTransportAvailableTb4b4b4(Te6edf3afterVpnLostTb4b4b4)Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ffwifi_lost_leaving_vpn_then_local_availableTb4b4b4(Tb4b4b4) Tb4b4b4{
T8b949e// Wi-Fi drops but the VPN (now riding cellular) still reaches the node: banner stays cleared.
Tff7b72val Te6edf3duringWifi Tff7b72=
Te6edf3listOfTb4b4b4(
Te6edf3NetworkTransportInfoTb4b4b4(Te6edf3hasWifi Tff7b72= Tff7b72trueTb4b4b4, Te6edf3hasEthernet Tff7b72= Tff7b72falseTb4b4b4, Te6edf3hasVpn Tff7b72= Tff7b72falseTb4b4b4)Tb4b4b4,
Te6edf3NetworkTransportInfoTb4b4b4(Te6edf3hasWifi Tff7b72= Tff7b72falseTb4b4b4, Te6edf3hasEthernet Tff7b72= Tff7b72falseTb4b4b4, Te6edf3hasVpn Tff7b72= Tff7b72trueTb4b4b4)Tb4b4b4,
Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3anyNetworkScanTransportAvailableTb4b4b4(Te6edf3duringWifiTb4b4b4)Tb4b4b4)
Tff7b72val Te6edf3afterWifiLost Tff7b72= Te6edf3listOfTb4b4b4(Te6edf3NetworkTransportInfoTb4b4b4(Te6edf3hasWifi Tff7b72= Tff7b72falseTb4b4b4, Te6edf3hasEthernet Tff7b72= Tff7b72falseTb4b4b4, Te6edf3hasVpn Tff7b72= Tff7b72trueTb4b4b4)Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3anyNetworkScanTransportAvailableTb4b4b4(Te6edf3afterWifiLostTb4b4b4)Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ffvpn_disabled_leaving_cellular_only_then_local_unavailableTb4b4b4(Tb4b4b4) Tb4b4b4{
T8b949e// The closing bug-report case: ZeroTier disabled, only cellular remains โ€” banner returns.
Tff7b72val Te6edf3withVpn Tff7b72=
Te6edf3listOfTb4b4b4(
Te6edf3NetworkTransportInfoTb4b4b4(Te6edf3hasWifi Tff7b72= Tff7b72falseTb4b4b4, Te6edf3hasEthernet Tff7b72= Tff7b72falseTb4b4b4, Te6edf3hasVpn Tff7b72= Tff7b72falseTb4b4b4)Tb4b4b4,
Te6edf3NetworkTransportInfoTb4b4b4(Te6edf3hasWifi Tff7b72= Tff7b72falseTb4b4b4, Te6edf3hasEthernet Tff7b72= Tff7b72falseTb4b4b4, Te6edf3hasVpn Tff7b72= Tff7b72trueTb4b4b4)Tb4b4b4,
Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3anyNetworkScanTransportAvailableTb4b4b4(Te6edf3withVpnTb4b4b4)Tb4b4b4)
Tff7b72val Te6edf3afterVpnDisabled Tff7b72= Te6edf3listOfTb4b4b4(Te6edf3NetworkTransportInfoTb4b4b4(Te6edf3hasWifi Tff7b72= Tff7b72falseTb4b4b4, Te6edf3hasEthernet Tff7b72= Tff7b72falseTb4b4b4, Te6edf3hasVpn Tff7b72= Tff7b72falseTb4b4b4)Tb4b4b4)
Te6edf3assertFalseTb4b4b4(Te6edf3anyNetworkScanTransportAvailableTb4b4b4(Te6edf3afterVpnDisabledTb4b4b4)Tb4b4b4)
Tb4b4b4}
Tb4b4b4}

T8b949e/**
* Coverage for the [shouldShowWifiUnavailableBanner] gate consumed by `ConnectionsScreen`. The banner surfaces "no
* usable transport for NSD/mDNS scan" only while a network scan is actively running, and is suppressed once the scan
* has produced discovered TCP nodes โ€” at that point the user has found what they were looking for and the recovery hint
* is no longer useful. Each case mirrors one of the reported combinations of (scan-active, results-empty) plus the
* permission and transport-availability guards.
*/
Tff7b72class T56d364WifiUnavailableBannerTest Tb4b4b4{
Tff7b72private Tff7b72fun Td2a8fftransportsTb4b4b4(Te6edf3wifiTb4b4b4: Tffa657Boolean Tff7b72= Tff7b72falseTb4b4b4) Tff7b72=
T8b949e// Reuses the predicate's encoding so the banner tests stay in lock-step with transport semantics.
Te6edf3listOfTb4b4b4(Te6edf3NetworkTransportInfoTb4b4b4(Te6edf3hasWifi Tff7b72= Te6edf3wifiTb4b4b4, Te6edf3hasEthernet Tff7b72= Tff7b72falseTb4b4b4, Te6edf3hasVpn Tff7b72= Tff7b72falseTb4b4b4)Tb4b4b4)

Tf0883e@Test
Tff7b72fun Td2a8ffscan_inactive_permission_granted_cellular_only_then_banner_hiddenTb4b4b4(Tb4b4b4) Tb4b4b4{
T8b949e// Pins the user-reported bug: no scan is running, permission granted, cellular-only
T8b949e// transport. The banner must NOT render โ€” the user is not actively trying to discover, so
T8b949e// the recovery hint is noise.
Te6edf3assertFalseTb4b4b4(
Te6edf3shouldShowWifiUnavailableBannerTb4b4b4(
Te6edf3isNetworkScanning Tff7b72= Tff7b72falseTb4b4b4,
Te6edf3localNetworkPermissionGranted Tff7b72= Tff7b72trueTb4b4b4,
Te6edf3wifiUnavailable Tff7b72= Tff7b72!Te6edf3anyNetworkScanTransportAvailableTb4b4b4(Te6edf3transportsTb4b4b4(Tb4b4b4)Tb4b4b4)Tb4b4b4,
Te6edf3discoveredTcpDevicesEmpty Tff7b72= Tff7b72trueTb4b4b4,
Tb4b4b4)Tb4b4b4,
Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ffscan_active_permission_granted_cellular_only_then_banner_showsTb4b4b4(Tb4b4b4) Tb4b4b4{
T8b949e// The auto-scan case: scan running in the background with cellular-only transport. Chip
T8b949e// visibility no longer gates this surface โ€” `isNetworkScanning` is the only activity
T8b949e// signal โ€” so the banner surfaces the missing-transport hint to the user.
Te6edf3assertTrueTb4b4b4(
Te6edf3shouldShowWifiUnavailableBannerTb4b4b4(
Te6edf3isNetworkScanning Tff7b72= Tff7b72trueTb4b4b4,
Te6edf3localNetworkPermissionGranted Tff7b72= Tff7b72trueTb4b4b4,
Te6edf3wifiUnavailable Tff7b72= Tff7b72!Te6edf3anyNetworkScanTransportAvailableTb4b4b4(Te6edf3transportsTb4b4b4(Tb4b4b4)Tb4b4b4)Tb4b4b4,
Te6edf3discoveredTcpDevicesEmpty Tff7b72= Tff7b72trueTb4b4b4,
Tb4b4b4)Tb4b4b4,
Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ffscan_active_permission_granted_wifi_unavailable_then_banner_showsTb4b4b4(Tb4b4b4) Tb4b4b4{
T8b949e// Clean positive case: scan actively running, permission granted, WiFi unavailable, no
T8b949e// discovered nodes yet โ€” banner fires. This is the only combination the banner is meant
T8b949e// to surface.
Te6edf3assertTrueTb4b4b4(
Te6edf3shouldShowWifiUnavailableBannerTb4b4b4(
Te6edf3isNetworkScanning Tff7b72= Tff7b72trueTb4b4b4,
Te6edf3localNetworkPermissionGranted Tff7b72= Tff7b72trueTb4b4b4,
Te6edf3wifiUnavailable Tff7b72= Tff7b72trueTb4b4b4,
Te6edf3discoveredTcpDevicesEmpty Tff7b72= Tff7b72trueTb4b4b4,
Tb4b4b4)Tb4b4b4,
Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ffpermission_denied_then_banner_hidden_even_when_scanningTb4b4b4(Tb4b4b4) Tb4b4b4{
T8b949e// Permission-recovery flow on the scan toggle owns this surface; banner must not overlap.
Te6edf3assertFalseTb4b4b4(
Te6edf3shouldShowWifiUnavailableBannerTb4b4b4(
Te6edf3isNetworkScanning Tff7b72= Tff7b72trueTb4b4b4,
Te6edf3localNetworkPermissionGranted Tff7b72= Tff7b72falseTb4b4b4,
Te6edf3wifiUnavailable Tff7b72= Tff7b72!Te6edf3anyNetworkScanTransportAvailableTb4b4b4(Te6edf3transportsTb4b4b4(Tb4b4b4)Tb4b4b4)Tb4b4b4,
Te6edf3discoveredTcpDevicesEmpty Tff7b72= Tff7b72trueTb4b4b4,
Tb4b4b4)Tb4b4b4,
Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ffwifi_present_then_banner_hidden_even_when_scanningTb4b4b4(Tb4b4b4) Tb4b4b4{
T8b949e// VPN off + Wi-Fi on + scanning โ†’ no banner (preserves the existing "transport available" outcome).
Te6edf3assertFalseTb4b4b4(
Te6edf3shouldShowWifiUnavailableBannerTb4b4b4(
Te6edf3isNetworkScanning Tff7b72= Tff7b72trueTb4b4b4,
Te6edf3localNetworkPermissionGranted Tff7b72= Tff7b72trueTb4b4b4,
Te6edf3wifiUnavailable Tff7b72= Tff7b72!Te6edf3anyNetworkScanTransportAvailableTb4b4b4(Te6edf3transportsTb4b4b4(Te6edf3wifi Tff7b72= Tff7b72trueTb4b4b4)Tb4b4b4)Tb4b4b4,
Te6edf3discoveredTcpDevicesEmpty Tff7b72= Tff7b72trueTb4b4b4,
Tb4b4b4)Tb4b4b4,
Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ffvpn_only_then_banner_hidden_even_when_scanningTb4b4b4(Tb4b4b4) Tb4b4b4{
T8b949e// VPN on + Wi-Fi off + scanning โ†’ no banner (VPN is a valid reachability path).
Tff7b72val Te6edf3vpnOnly Tff7b72= Te6edf3listOfTb4b4b4(Te6edf3NetworkTransportInfoTb4b4b4(Te6edf3hasWifi Tff7b72= Tff7b72falseTb4b4b4, Te6edf3hasEthernet Tff7b72= Tff7b72falseTb4b4b4, Te6edf3hasVpn Tff7b72= Tff7b72trueTb4b4b4)Tb4b4b4)
Te6edf3assertFalseTb4b4b4(
Te6edf3shouldShowWifiUnavailableBannerTb4b4b4(
Te6edf3isNetworkScanning Tff7b72= Tff7b72trueTb4b4b4,
Te6edf3localNetworkPermissionGranted Tff7b72= Tff7b72trueTb4b4b4,
Te6edf3wifiUnavailable Tff7b72= Tff7b72!Te6edf3anyNetworkScanTransportAvailableTb4b4b4(Te6edf3vpnOnlyTb4b4b4)Tb4b4b4,
Te6edf3discoveredTcpDevicesEmpty Tff7b72= Tff7b72trueTb4b4b4,
Tb4b4b4)Tb4b4b4,
Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ffscan_active_permission_granted_wifi_unavailable_results_found_then_banner_hiddenTb4b4b4(Tb4b4b4) Tb4b4b4{
T8b949e// Once the live scan has produced discovered TCP nodes, the recovery hint is no longer
T8b949e// useful โ€” the user has found what they were looking for. Banner stays hidden even though
T8b949e// scan is active, permission is granted, and WiFi is reported unavailable.
Te6edf3assertFalseTb4b4b4(
Te6edf3shouldShowWifiUnavailableBannerTb4b4b4(
Te6edf3isNetworkScanning Tff7b72= Tff7b72trueTb4b4b4,
Te6edf3localNetworkPermissionGranted Tff7b72= Tff7b72trueTb4b4b4,
Te6edf3wifiUnavailable Tff7b72= Tff7b72trueTb4b4b4,
Te6edf3discoveredTcpDevicesEmpty Tff7b72= Tff7b72falseTb4b4b4,
Tb4b4b4)Tb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tb4b4b4}

Served by rngit 1.5.0 - Generated in 0.08s